"""Build DFuse"""
import os

HEADERS = ['ioil_io.h', 'ioil_defines.h', 'ioil_api.h', 'ioil.h']
COMMON_SRC = ['dfuse_obj_da.c', 'dfuse_vector.c']
DFUSE_SRC = ['dfuse_core.c',
             'dfuse_main.c',
             'dfuse_fuseops.c',
             'inval.c',
             'dfuse_cont.c',
             'dfuse_thread.c',
             'dfuse_pool.c']
OPS_SRC = ['create',
           'fgetattr',
           'forget',
           'getxattr',
           'listxattr',
           'ioctl',
           'lookup',
           'mknod',
           'open',
           'opendir',
           'read',
           'rename',
           'readdir',
           'readlink',
           'removexattr',
           'setxattr',
           'setattr',
           'symlink',
           'unlink',
           'write',
           'statfs']

IOIL_SRC = ['int_posix.c', 'int_read.c', 'int_write.c']
PIL4DFS_SRC = ['int_dfs.c', 'hook.c', 'dfs_dcache', 'aio.c']


def build_common(env, files, is_shared):
    """Build the common objects as shared or static"""
    common = []

    for src_file in files:
        if is_shared:
            common += env.SharedObject(src_file, SHOBJPREFIX='s_')
        else:
            common += env.Object(src_file)

    return common


def build_client_libs_shared(env, prereqs):
    """build the shared interception library"""

    ilenv = env.Clone()
    ilenv.AppendUnique(CFLAGS=['-fPIC'])
    ilenv.AppendUnique(CPPDEFINES=['IOIL_PRELOAD'])
    ilenv.AppendUnique(LIBPATH=[Dir('../dfs')])
    ilenv.AppendUnique(LIBPATH=[Dir('../api')])
    ilenv.AppendUnique(LIBS=['dfs'])
    penv = ilenv.Clone()
    penv.AppendUnique(CPPDEFINES=['_FILE_OFFSET_BITS=64'])

    il_obj = []
    for src in IOIL_SRC:
        il_obj += ilenv.SharedObject(os.path.join('il', src), SHOBJPREFIX='s_')

    pil4dfsenv = env.Clone()
    if '-fvisibility=hidden' in pil4dfsenv['CFLAGS']:
        pil4dfsenv['CFLAGS'].remove('-fvisibility=hidden')
    pil4dfsenv.AppendUnique(LIBPATH=[Dir('../dfs')])
    pil4dfsenv.AppendUnique(CPPPATH=[Dir('../dfs').srcnode()])
    pil4dfsenv.AppendUnique(LIBPATH=[Dir('../../gurt')])
    pil4dfsenv.AppendUnique(CPPPATH=[Dir('../../gurt').srcnode()])
    pil4dfsenv.AppendUnique(LIBPATH=[Dir('../api')])
    pil4dfsenv.AppendUnique(LIBS=['pthread', 'daos', 'dfs', 'duns', 'gurt', 'dl'])
    pil4dfsenv.require('capstone')
    pil4dfs_obj = []
    for src in PIL4DFS_SRC:
        pil4dfs_obj += pil4dfsenv.SharedObject(os.path.join('pil4dfs', src))

    common = build_common(penv, COMMON_SRC, True)

    # Now build the interception library
    il_lib = ilenv.d_library('il/libioil', il_obj + common)
    env.InstallVersionedLib(os.path.join("$PREFIX", 'lib64'), il_lib)
    dfuse_lib = ilenv.d_library('dfuse', common)

    pil4dfs_lib = pil4dfsenv.d_library('pil4dfs/libpil4dfs', pil4dfs_obj)
    env.InstallVersionedLib(os.path.join("$PREFIX", 'lib64'), pil4dfs_lib)

    gen_script = ilenv.d_program('il/gen_script', ['il/gen_script.c'], LIBS=[])
    if prereqs.test_requested():
        script = ilenv.Command('il/check_ioil_syms', gen_script,
                               "$SOURCE -s $TARGET")
        env.Install('$PREFIX/lib/daos/TESTING/scripts', script)
    script = ilenv.Command('il/ioil-ld-opts', gen_script,
                           '$SOURCE -l $TARGET')
    env.Install('$PREFIX/share/daos', script)
    env.InstallVersionedLib(os.path.join("$PREFIX", 'lib64'), dfuse_lib)

    Default(dfuse_lib, il_lib)


def build_client_libs_static(env, common):
    """build the static interception library"""

    ilenv = env.Clone()

    il_obj = []
    for src in IOIL_SRC:
        il_obj += ilenv.Object(os.path.join('il', src))

    # Now build the interception library
    il_lib = ilenv.d_static_library(target='il/libioil.a', source=il_obj + common, hide_syms=True)
    env.Install('$PREFIX/lib64', il_lib)

    # Now build the dfuse library
    dfuse_lib = ilenv.d_static_library(target='libdfuse.a', source=common, hide_syms=True)
    env.Install('$PREFIX/lib64', dfuse_lib)

    Default(il_lib, dfuse_lib)


def check_struct_member(context, text, struct, member):
    """scons check for a struct member existing"""

    context.Message(f'Checking for {member} in {struct} ')

    # pylint: disable-next=consider-using-f-string
    src = '{0}\n{1} val = {{.{2} = 0}};\nint main() {{}}'.format(text, struct, member)
    rc = context.TryCompile(src, '.c')
    context.Result(rc)
    return rc


def check_ioctl_def(context, ctype):
    """scons check for a struct member existing"""

    context.Message(f'Checking if fuse ioctl is type {ctype} ')

    # pylint: disable-next=consider-using-f-string
    src = """#include <fuse3/fuse_lowlevel.h>

extern void
my_ioctl (fuse_req_t req, fuse_ino_t ino, %s cmd,
    void *arg, struct fuse_file_info *fi, unsigned flags,
    const void *in_buf, size_t in_bufsz, size_t out_bufsz);

struct fuse_lowlevel_ops ops = {.ioctl = my_ioctl};

""" % ctype  # pylint: disable=consider-using-f-string

    rc = context.TryCompile(src, '.c')
    context.Result(rc)
    return rc


def configure_fuse(cenv):
    """Configure for specific version of fuse"""
    if GetOption('help') or GetOption('clean'):
        return

    check = Configure(cenv,
                      custom_tests={'CheckFuseIoctl': check_ioctl_def})

    if check.CheckFuseIoctl('unsigned int'):
        pass
    elif check.CheckFuseIoctl('int'):
        cenv.AppendUnique(CPPDEFINES={'FUSE_IOCTL_USE_INT': '1'})
    else:
        print('Could not determine type of fuse ioctl type')
        Exit(2)

    check.Finish()


def find_static_fuse(cenv):
    """Find and copy static fuse library"""

    fuse_prefix = '/usr'
    if os.path.exists(cenv.subst('$FUSE_PREFIX')):
        fuse_prefix = cenv.subst('$FUSE_PREFIX')

    for path in ['lib64', 'lib/x86_64-linux-gnu', 'lib']:
        fuselib = os.path.join(fuse_prefix, path, "libfuse3.a")
        if os.path.exists(fuselib):
            return [File(fuselib)]

    print("Could not find libfuse3.a")
    return []


def scons():
    """Scons function"""

    Import('env', 'prereqs')

    if env['CC'] == 'gcc':
        il_env = env.Clone()
    else:
        Import('base_env')
        il_env = base_env.Clone()
        il_env['CC'] = 'gcc'
        il_env['CXX'] = None
        il_env.compiler_setup()

    # Set options which are used throughout the src.
    dfuse_env = env.Clone(LIBS=[])
    dfuse_env.AppendUnique(CPPPATH=[Dir('.').srcnode()])
    dfuse_env.AppendUnique(CFLAGS=['-pthread'])
    dfuse_env.AppendUnique(LIBS=['pthread', 'daos', 'daos_common', 'uuid', 'm'])

    gcc_env = il_env.Clone(LIBS=[])
    gcc_env.AppendUnique(CPPPATH=[Dir('.').srcnode()])
    gcc_env.AppendUnique(CFLAGS=['-pthread', '-fvisibility=hidden'])
    gcc_env.AppendUnique(LIBS=['pthread', 'daos', 'daos_common'])

    build_client_libs_shared(gcc_env, prereqs)

    static_env = gcc_env.Clone()

    # Build a static library of the common parts.
    common = build_common(dfuse_env, COMMON_SRC, False)
    build_client_libs_static(static_env, common)

    cenv = dfuse_env.Clone()
    cenv.AppendUnique(LIBPATH=[Dir('../dfs')])
    cenv.AppendUnique(LIBS=['dfs', 'duns'])

    cenv.require('hwloc')
    if cenv.get('STATIC_FUSE'):
        cenv.require('fuse', headers_only=True)
        static_fuse = find_static_fuse(cenv)
        extra_libs = ['dl']
    else:
        cenv.require('fuse')
        static_fuse = []
        extra_libs = ['dl']

    configure_fuse(cenv)

    dfuse_obj = []
    for src in DFUSE_SRC:
        dfuse_obj += cenv.Object(src)
    for src in OPS_SRC:
        dfuse_obj += cenv.Object(os.path.join('ops', f'{src}.c'))
    cenv.AppendUnique(LIBS=['gurt'] + extra_libs)
    dfuse_bin = cenv.d_program('dfuse/dfuse', common + dfuse_obj + static_fuse)

    Default(dfuse_bin)

    cenv.Install(os.path.join("$PREFIX", 'bin'), dfuse_bin)


if __name__ == "SCons.Script":
    scons()
